package com.advision.android;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Log;

/**
 * Provides data required to deliver ads.  As a developer if you know more about the user,
 * such as their gender or birthday, you can set those attributes here to deliver more relevant
 * advertisements to your users.
 */
public class AdManager
{
	/**
	 * Male and Female genders.
	 */
	public enum Gender
	{
		MALE,
		FEMALE
	}
	
	/**
	 * This SDK version.  It is passed in every ad request in the <code>client_sdk</code>
	 * field.  The server can use this for any version-specific logic.
	 */
	public static final String SDK_VERSION = "20081201-ANDROID-924bc8a810aed766";
	
	/**
	 * The site ID used by our examples.  This should never be used by anything other than
	 * our examples or else the publisher won't be credited for ads.
	 */
	static final String SDK_SITE_ID = "a1496ced2842262";

	/**
	 * The category all AdMob SDK logging falls under in Android's logging console.
	 */
	static final String LOG = "AdMob SDK";
	
	/**
	 * Flag indicating if this phone should show "Ads by Admob" if there is space.  One
	 * reason we might not show it is if the user is Japanese since our text is in English.
	 */
	private static boolean showAdsByAdmob;
	
	/**
	 * The User-Agent HTTP header for this phone.
	 * <p>
	 * By default Android makes this "Java0" when using HTTP classes directly.  However,
	 * we need it to be the browser's user agent so AdMob servers can optimize this ad.
	 */
	private static String userAgent;

	/**
	 * This application's AdMob ID.
	 */
	private static String publisherId;

	/**
	 * A unique ID for this user that helps show a variety of ads instead of the same
	 * one over and over.
	 */
	private static String userId;
	
	/**
	 * A flag the application can set to turn user ID detection off.  Since ID detection requires
	 * access to the phone some authors may want to disable it so their installation does not
	 * prompt the user for it.
	 */
	private static boolean detectUserId = true;
	
	/**
	 * The user's location to show geotargeted ads to.
	 */
	private static Location coordinates;
	
	/**
	 * The user's birthday to show ads targeted to certain age ranges.
	 */
	private static GregorianCalendar birthday;
	
	/**
	 * The gender of the user to show ads targeted to men or women.
	 */
	private static Gender gender;

	/**
	 * Delivers a critical error message to the user.  These errors mean the ad cannot work.
	 * For example if the publisher ID is not configured correctly.
	 * 
	 * @param message is a the error message for the user.  It should be less than 50 characters
	 *  if possible.
	 */
	protected static void clientError( String message )
	{
		// Log it to the LogCat window.
		Log.e( LOG, message );

		// Raise an exception.
		throw new IllegalArgumentException( message );
	}
	
	/**
	 * Gets the unique ID assigned to this application by AdMob.
	 * 
	 * @param context is the application's context used to get the user's location.
	 * @return The publisher ID for this application.
	 */
	public static String getPublisherId( Context context )
	{
		if ( publisherId == null )
		{
			// Read the Publisher ID from AndroidManifest.xml.  It is under the <application> tag and looks like:
			//	<meta-data android:name="ADMOB_PUBLISHER_ID" android:value="a1496ced2842262" />
			try
			{
				PackageManager manager = context.getPackageManager();
				ApplicationInfo info = manager.getApplicationInfo( context.getPackageName(), PackageManager.GET_META_DATA );
				
				if ( info != null )
				{
					String id = info.metaData.getString( "ADMOB_PUBLISHER_ID" );
					Log.d( LOG, "Publisher ID read from AndroidManifest.xml is " + id );

					if ( id.equals( SDK_SITE_ID ) &&
						 (context.getPackageName().equals( "com.admob.android.test" ) ||
						  context.getPackageName().equals( "com.example.admob.lunarlander" )) )
					{
						// Special case.  This is the sample application so allow the sample publisher ID.
						Log.i( LOG, "This is a sample application so allowing sample publisher ID." );
						publisherId = id;
					}
					else
					{
						setPublisherId( id );
					}
				}
			}
			catch (Exception e)
			{
				Log.e( LOG, "Could not read ADMOB_PUBLISHER_ID meta-data from AndroidManifest.xml.", e );
			}
		}
		
		return publisherId;
	}
	
	/**
	 * Programatic way to sets the unique publisher ID assigned to this application by AdMob.
	 * Typically, however, it is set in your AndroidManifest.xml by adding it within the
	 * "<code>&lt;/application&gt</code>" tag:
	 * <pre><code>
	 * 	&lt;meta-data android:name="ADMOB_PUBLISHER_ID" android:value="Your Publisher ID here" /&gt;
	 * </code></pre>
	 * 
	 * @param id is the publisher ID for this application.
	 */
	public static void setPublisherId( String id )
	{
		if ( (id == null) || (id.length() != 15) )
		{
			clientError( "Incorrect AdMob publisher ID.  Should 15 [a-f,0-9] characters:  " + publisherId );
		}

		if ( id.equalsIgnoreCase( SDK_SITE_ID ) )
		{
			// Do not allow publishers to use the sample ID by accident.
			clientError( "Cannot use the sample publisher ID (" + SDK_SITE_ID + ").  Yours is available on www.admob.com." );
		}
		
		Log.i( LOG, "Publisher ID set to " + id );
		
		AdManager.publisherId = id;
	}
	
	/**
	 * Returns a user ID.  It is generated by properties unique to this phone.
	 * <p>
	 * For this method to work make sure your application has phone state permissions by
	 * copy-and-pasting the following line into manifest.xml just before the
	 * final tag "<code>&lt;/manifest&gt</code>":
	 * <pre><code>
	 * 	&lt;uses-permission android:name="android.permission.READ_PHONE_STATE" /&gt;
	 * </code></pre>
	 * 
	 * @param context is the application's context used to get a unique ID for this user.
	 * @return The unique user ID.
	 */
	public static String getUserId( Context context )
	{
		if ( userId == null )
		{
			// Verify phone state permission is available.  Otherwise we cannot get the IMEI number
			// to identify the user.
			if ( context.checkCallingOrSelfPermission( android.Manifest.permission.READ_PHONE_STATE ) == PackageManager.PERMISSION_DENIED )
			{
				if ( detectUserId )
				{
					clientError( "Cannot get a user ID.  Open manifest.xml and just before the final </manifest> tag add:  " +
						   "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\" /> or call disableIdDetection()." );
				}
			}
			else  // Telephone permission granted
			{
				// Get the IMEI number of the phone.
				TelephonyManager manager = (TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE );
				
				if ( manager != null )
				{
					userId = manager.getDeviceId();
					
					// Make the ID the standard AdMob 32 character length.
					userId = md5( userId );
					
					Log.i( LOG, "The user ID is " + userId );
				}
				else
				{
					Log.w( LOG, "No user ID because the telephony manager was not available." );
				}
			}
		}
		
		return userId;
	}
	
	/**
	 * Disables ID detection and the need for <code>android.permission.READ_PHONE_STATE</code> permissions.
	 * If your application does not have phone permissions call this method once before requesting any ads.
	 * <p>
	 * <i>Please note calling this method will significantly decrease the amount of money you make.</i>
	 * Uniquely identifying a user stops them from seeing the same ad over-and-over.  So only call this
	 * method if you absolutely do not want to add <code>android.permission.READ_PHONE_STATE</code> permission
	 * to your application.
	 */
	public static void disableIdDetection()
	{
		detectUserId = false;
	}
	
	/**
	 * Utility method for hashing any string into a 32 character string.  Useful for
	 * creating a unique phone signature.
	 * <p>
	 * Method taken directly from the Pub Code.
	 */
	private static String md5( String val )
	{
		String result = null;
		
		if ( val != null && val.length() > 0 )
		{
			try
			{
				MessageDigest md5 = MessageDigest.getInstance( "MD5" );
				md5.update( val.getBytes(), 0, val.length() );
				result = String.format( "%032X", new BigInteger( 1, md5.digest() ) );
			}
			catch (Exception e)
			{
				Log.d( LOG, "Could not generate hash of " + val, e );
				
				// Not perfect, but trim to 32 chars.
				userId = userId.substring( 0, 32 );
			}
		}
		
		return result;
	}
	
	/**
	 * Returns the last known location of the user.
	 * <p>
	 * For this to return data the application needs permission to access the location
	 * services of the phone.  Copy-and-paste one of the following lines into manifest.xml just before the
	 * final tag "<code>&lt;/manifest&gt</code>":
	 * <pre><code>
	 * 	&lt;uses-permission android:name="android.permission.ACCESS_COURSE_LOCATION" /&gt;
	 * 	&lt;uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /&gt;
	 * </code></pre>
	 * 
	 * @param context is the application's context used to get the user's location.
	 * @return The last known location of the user or <code>null</code> if
	 *  it is not known.
	 */
	public static Location getCoordinates( Context context )
	{
		if ( (coordinates == null) && (context != null) )
		{
			synchronized ( context )
			{
				boolean permissions = false;
				LocationManager manager = null;
				String provider = null;
				
				// First try to get access to the network triangulation location.
				if ( context.checkCallingOrSelfPermission( android.Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED )
				{
					Log.d( LOG, "Trying to get locations from the network." );
					
					permissions = true;
					manager = (LocationManager) context.getSystemService( Context.LOCATION_SERVICE );
					
					if ( manager != null )
					{
						Criteria criteria = new Criteria();
						criteria.setAccuracy( Criteria.ACCURACY_COARSE );
						criteria.setCostAllowed( false );
						provider = manager.getBestProvider( criteria, true );
					}
				}
				
				// If we did not get GPS access, try to get location through GPS.
				if ( provider == null )
				{
					if ( context.checkCallingOrSelfPermission( android.Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED )
					{
						Log.d( LOG, "Trying to get locations from GPS." );
						
						permissions = true;
						manager = (LocationManager) context.getSystemService( Context.LOCATION_SERVICE );
						
						if ( manager != null )
						{
							Criteria criteria = new Criteria();
							criteria.setAccuracy( Criteria.ACCURACY_FINE );
							criteria.setCostAllowed( false );
							provider = manager.getBestProvider( criteria, true );
						}
					}
				}
	
				// Did we get an active location provider?
				if ( permissions == false )
				{
					Log.w( LOG, "Cannot access user's location.  To improve your revenue open manifest.xml and just before the final </manifest> tag add:  <uses-permission android:name=\"android.permission.ACCESS_COURSE_LOCATION\" />" );
				}
				else if ( provider == null )
				{
					Log.d( LOG, "No location providers are available.  Ads will not be geotargeted." );
				}
				else  // provider != null
				{
					Log.i( LOG, "Location provider setup successfully." );
					
					coordinates = manager.getLastKnownLocation( provider );
					
					// Add a location listener for better accuracy and future updates.
					final long minTime = 15 * 60 * 1000;  // 15 minutes
					final float minDistance = 200;  // 200 meters or about a city block
					
					manager.requestLocationUpdates( provider, minTime, minDistance, new LocationListener()
						{
							public void onLocationChanged( Location location )
							{
								// Update the user's location.
								AdManager.coordinates = location;
							}
							
							public void onProviderDisabled( String provider )
							{
								// Don't care if we stop getting location updates.
							}
							
							public void onProviderEnabled( String provider )
							{
								// Don't care until we get a location update.
							}
							
							public void onStatusChanged( String provider, int status, Bundle extras )
							{
								// Don't care.
							}
						},
						context.getMainLooper() );
				}
			}
		}
		
		return coordinates;
	}
	
	/**
	 * Returns the last known location of the user as a string suitable for
	 * consumption by AdMob servers.
	 * 
	 * @param context is the application's context used to get the user's location.
	 * @return The last known location of the user or <code>null</code> if
	 *  it is not known.
	 */
	static String getCoordinatesAsString( Context context )
	{
		String result = null;
		Location l = getCoordinates( context );
		
		if ( l != null )
		{
			result = l.getLatitude() + "," + l.getLongitude();
		}

		if ( Log.isLoggable( LOG, Log.DEBUG ) )
		{
			Log.d( LOG, "User coordinates are " + result );
		}
	
		return result;
	}
	
	/**
	 * Returns the birthday of the user.  This is used to calculate their age.
	 * 
	 * @return The birthday of the user or <code>null</code> if it is not known.
	 */
	public static GregorianCalendar getBirthday()
	{
		return birthday;
	}
	
	/**
	 * Returns the birthday of the user as a string suitable for consumption by 
	 * AdMob servers.
	 * 
	 * @return The birthday of the user or <code>null</code> if it is not known.
	 */
	static String getBirthdayAsString()
	{
		String result = null;
		GregorianCalendar dob = getBirthday();
		
		if ( dob != null )
		{
			result = String.format( "%04d%02d%02d", 
					dob.get( Calendar.YEAR ),
					dob.get( Calendar.MONTH ) + 1,  // January starts at 0
					dob.get( Calendar.DAY_OF_MONTH ) );
		}
		
		return result;
	}
	
	/**
	 * Sets the birthday of the user.
	 * 
	 * @param birthday is the user's birthday or <code>null</code> if it is not known.
	 */
	public static void setBirthday( GregorianCalendar birthday )
	{
		AdManager.birthday = birthday;
	}
	
	/**
	 * Sets the birthday of the user.
	 * 
	 * @param year is the year they were born in (e.g. 1976).
	 * @param month is the month they were born in (e.g. 3 for March).
	 * @param day is the day they were born on (e.g. 13 for the 13th).
	 */
	public static void setBirthday( int year, int month, int day )
	{
		GregorianCalendar cal = new GregorianCalendar();
		cal.set( year, month - 1, day );
		setBirthday( cal );
	}

	/**
	 * Returns the user's gender.
	 * 
	 * @return The gender of the user or <code>null</code> if it is not known.
	 */
	public static Gender getGender()
	{
		return gender;
	}
	
	/**
	 * Returns the user's gender as a string suitable for consumption by 
	 * AdMob servers.
	 * 
	 * @return The gender of the user or <code>null</code> if it is not known.
	 */
	static String getGenderAsString()
	{
		if ( gender == Gender.MALE )
		{
			return "m";
		}
		else if ( gender == Gender.FEMALE )
		{
			return "f";
		}
		else
		{
			return null;
		}
	}
	
	/**
	 * Sets the gender of the user to either male or female.
	 * 
	 * @param gender is the user's gender or <code>null</code> if it is not known.
	 */
	public static void setGender( Gender gender )
	{
		AdManager.gender = gender;
	}
	
	/**
	 * Populate <code>showAdsByAdmob</code>. 
	 */
	static
	{
		boolean show = true;
		
		Locale l = Locale.getDefault();
		String language = l.getLanguage();
		
		if ( language != null )
		{
			if ( language.equalsIgnoreCase( "en" ) == false )
			{
				Log.d( LOG, "Will not show \"Ads by Admob\" since this phone is " + language + " and not English." );
				show = false;
			}
		}
		
		showAdsByAdmob = show;
	}
	
	/**
	 * Returns if "Ads by AdMob" is allowed to show on this phone or not.
	 * 
	 * @return <code>true</code> if this phone should show "Ads by Admob".
	 */
	static boolean showAdsByAdmob()
	{
		return showAdsByAdmob;
	}
	
	/**
	 * Generates the User-Agent HTTP header used by the browser on this phone.
	 * 
	 * @return The user-agent for the WebKit browser specific to this phone.
	 */
	static String getUserAgent()
	{
		if ( userAgent == null )
		{
			// This is copied from the Android source frameworks/base/core/java/android/webkit/WebSettings.java.
			// It is somewhat of a hack since Android does not expose this to us but AdMob servers need it
			// for ad picking.
			
			StringBuffer arg = new StringBuffer();
			
			// Add version
			final String version = Build.VERSION.RELEASE;
			if ( version.length() > 0 )
			{
				arg.append( version );
			}
			else
			{
				// default to "1.0"
				arg.append( "1.0" );
			}
			arg.append( "; " );
			
			// Initialize the mobile user agent with the default locale.
			final Locale l = Locale.getDefault();
			final String language = l.getLanguage();
			if ( language != null )
			{
				arg.append( language.toLowerCase() );
				final String country = l.getCountry();
				if ( country != null )
				{
					arg.append( "-" );
					arg.append( country.toLowerCase() );
				}
			}
			else
			{
				// default to "en"
				arg.append( "en" );
			}
			
			// Add device name
			final String device = Build.DEVICE;
			if ( device.length() > 0 )
			{
				arg.append( "; " );
				arg.append( device );
			}
			
			// Mozilla/5.0 (Linux; U; Android 1.0; en-us; dream) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2
			//final String base = context.getResources().getText(com.android.internal.R.string.web_user_agent).toString();
			final String base = "Mozilla/5.0 (Linux; U; Android %s) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2";
			userAgent = String.format( base, arg );
			
			Log.i( LOG, "Phone's user-agent is:  " + userAgent );
		}
		
		return userAgent;
	}
}
